Spring基础知识:IoC DI
参考资料
Spring 的官方文档 Spring IoC有什么好处呢? - Mingqi的回答 - 知乎
Spring 是一个开源的免费框架(容器) Spring 是一个 非入侵式的 框架(引入 Spring 不会对现有代码产生影响) 控制反转(IOC),面向切面编程(AOP)
配置环境
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
什么是 IoC
实际上 IoC 就是设计模式中的依赖倒转原则
依赖倒转原则:
- 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象。
- 抽象不能依赖于具象,具象依赖于抽象。
什么是依赖倒置原则? 假设我们设计一辆汽车:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个“依赖”关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。
这样的设计看起来没问题,但是可维护性却很低。假设设计完工之后,上司却突然说根据市场需求的变动,要我们把车子的轮子设计都改大一码。这下我们就蛋疼了:因为我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改;同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改——整个设计几乎都得改!
我们现在换一种思路。我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身, 车身依赖汽车。
这时候,上司再说要改动轮子的设计,我们就只需要改动轮子的设计,而不需要动底盘,车身,汽车的设计了。
这就是依赖倒置原则——把原本的高层建筑依赖底层建筑“倒置”过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的“牵一发动全身”的情况。
IoC 与 DI 的关系
控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。其实这些概念初次接触都会感到云里雾里的。说穿了,这几种概念的关系大概如下:
为了理解这几个概念,我们还是用上面汽车的例子。只不过这次换成代码。我们先定义四个Class,车,车身,底盘,轮胎。然后初始化这辆车,最后跑这辆车。代码结构如下:
这样,就相当于上面第一个例子,上层建筑依赖下层建筑——每一个类的构造函数都直接调用了底层代码的构造函数。假设我们需要改动一下轮胎(Tire)类,把它的尺寸变成动态的,而不是一直都是30。我们需要这样改:
由于我们修改了轮胎的定义,为了让整个程序正常运行,我们需要做以下改动:
由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!在软件工程中,这样的设计几乎是不可维护的——在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。
所以我们需要进行控制反转(IoC),及上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这里我们用构造方法传递的依赖注入方式重新写车类的定义:
这里我们再把轮胎尺寸变成动态的,同样为了让整个系统顺利运行,我们需要做如下修改:
这里只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。
不仅如此,在实际的工程中,这种设计模式还有利于不同组的协同合作和单元测试:比如开发这四个类的分别是四个不同的组,那么只要定义好了接口,四个不同的组可以同时进行开发而不相互受限制;而对于单元测试,如果我们要写 Car类的单元测试,就只需要 Mock一下 Framework 类传入 Car就行了,而不用把 Framework, Bottom, Tire 全部 new一遍再来构造Car。
这里我们是采用的构造函数传入的方式进行的依赖注入。其实还有另外两种方法:Setter传递和接口传递。这里就不多讲了,核心思路都是一样的,都是为了实现控制反转。
什么是控制反转容器
那什么是控制反转容器(IoC Container)呢?其实上面的例子中,对车类进行初始化的那段代码发生的地方,就是控制反转容器。
因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的 new。这里 IoC 容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,只需要维护一个 Configuration(可以是 xml 可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。这是引入 IoC Container 的第一个好处。
IoC Container的第二个好处是:在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车 instance 时候,是从底层往上层 new 的:
这个过程中,我们需要了解整个 Car/Framework/Bottom/Tire 类构造函数是怎么定义的,才能一步一步 new 注入。
而 IoC Container 在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步 new(有点像深度优先遍历):
这里 IoC Container 可以直接隐藏具体的创建实例的细节,在我们来看它就像一个工厂:
我们就像是工厂的客户。我们只需要向工厂请求一个 Car 实例,然后它就给我们按照 Config 创建了一个 Car 实例。我们完全不用管这个 Car 实例是怎么一步一步被创建出来。
简单实现一个例子
Inversion of Control(控制反转)是一种把创建对象的主动权交给使用者的思想(具体看下面这个案例),而依赖注入(Dependency Injection)只是其实现方式
// IoC 的例子
public class UserServiceImp implements UserService {
// 这里只使用接口来接受 “注入” 的实现类
private UserDao userDao;
/**
* 当用户使用这个 UserServiceImp 时利用 set 动态注入一个 userDao 的实现类,
* 而不是 new UserDaoImp()的形式在这个业务类里去创建一个 UserDaoImp 实现类
* 省的如果想要更换其他的 UserDaoImp 时还需要手动修改代码
*
* 而这种在使用时才传入一个实现类的方式叫做 “控制反转”
*/
@Override
public UserService setUserDao(UserDao userDao) {
this.userDao = userDao;
return this;
}
// 通过注入的 UserDaoImp 来取得 User 对象
@Override
public void getUser(){
userDao.getUser();
}
}
通过上面的例子可以看到系统的耦合性大大降低了,可以更加专注在业务的实现上,因为业务可以无需知道注入进来的是哪个实现类,只需使用这个接口去接收这个实现类就行了(里氏替换原则)
依赖注入:DI
Dependency Injection 即 “依赖注入”,其是 Spring 实现控制反转的方式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 使用spring来创建对象,在spring这些都称为Bean-->
<bean id="hello" class="com.alsritter.pojo.Hello">
<property name="str" value="Spring"/>
</bean>
</beans>
将实例对象委托给第三方,无需自己亲自去创建这个对象。一般搭配接口使用,使之可以不关心自动注入进来的实现类是哪个
public static void main(String[] args) {
//获取 Spring 的上下文对象
//这配置文件可以传入多个 public ClassPathXmlApplicationContext(String... configLocations)
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Hello hello = (Hello) context.getBean("hello");
}
IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象
Set 方式注入
- ref:引用容器中创建好的对象
- value:传入的是基础数据类型
在 Java 中使用这个 Context 来获取交给容器管理的 Bean
<bean id="address" class="com.alsritter.pojo.Address"/>
<bean id="student" class="com.alsritter.pojo.Student">
<!-- 普通注入-->
<property name="name" value="测试名"/>
<!-- Bean 注入-->
<property name="address" ref="address"/>
<!-- 数组注入-->
<property name="books">
<array>
<value>红楼梦</value>
<value>西游记</value>
<value>水浒传</value>
<value>三国演义</value>
</array>
</property>
<!-- List注入-->
<property name="hobbies">
<list>
<value>听歌</value>
<value>敲代码</value>
<value>玩游戏</value>
</list>
</property>
<!-- Map注入-->
<property name="card">
<map>
<entry key="身份证" value="111111"/>
<entry key="银行卡" value="222222"/>
</map>
</property>
<!-- Set注入-->
<property name="games">
<set>
<value>女神异闻录</value>
<value>合金装备</value>
</set>
</property>
<!-- 传入一个null-->
<property name="wife">
<null/>
</property>
<!-- Properties配置文件,就像那个sql的那个一样-->
<property name="info">
<props>
<prop key="driver">111111</prop>
<prop key="url">url...</prop>
<prop key="username">root</prop>
<prop key="password">1234</prop>
</props>
</property>
</bean>
同理,添加一个实现类也无需动接口,直接在配置文件里添加对应的实现类 Bean 就好了
注意,这样创建的 Bean 是单例模式的
<bean id="mysqlImp" class="com.alsritter.dao.UserMysqlDaoImp"/>
<bean id="oracleImp" class="com.alsritter.dao.UserDaoOracleImp"/>
<bean id="defaultImp" class="com.alsritter.dao.UserDaoImp"/>
<bean id="userServiceImp" class="com.alsritter.service.UserServiceImp">
<property name="userDao" ref="defaultImp"/>
</bean>
构造器注入
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="hight" value="20"/>
<constructor-arg name="width" value="42"/>
</bean>
用拓展工具注入
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
p 命名空间 注入,可以直接注入属性的值
<!-- 实际上就是property的缩写 -->
<!-- 原本 -->
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="someone@somewhere.com"/>
</bean>
<!-- 使用了p命名空间 -->
<bean name="p-namespace" class="com.example.ExampleBean" p:email="someone@somewhere.com"/>
c 命名空间 注入
<!-- 实际上就是constructor的缩写 -->
<!-- 原本 -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>
<!-- 使用了c命名空间 -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>
Bean 的作用域
Scope | Description |
---|---|
singleton | 单例模式 (默认就是单例模式) |
prototype | 原型模式 每次从容器中get的时候,都会产生一个新对象 |
request | 将单个 Bean 定义限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例, |
session | 将单个 Bean 定义限定为 HTTP 会话的生命周期。 |
application | 将单个Bean 定义限定为 ServletContext 的生命周期。 |
websocket | 将单个 Bean 定义限定为 WebSocket 的生命周期。 |
request、session、application、websocket 仅在支持web的Spring应用程序上下文中有效。
Example
<bean id="student2" class="com.alsritter.pojo.Student" p:name="张三" scope="prototype"/>
因为使用了原型模式,所以每取得一次就创建一个
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Student student = (Student) context.getBean("student2");
Student student2 = (Student) context.getBean("student2");
System.out.println(student == student2);
}
-->
false
注解配置 Bean
需要先添加这个扫描标签
<!-- 指定要扫描的包,这个包下的注解就会生效 -->
<context:component-scan base-package="com.alsritter.pojo"/>
可以直接使用 @Component
注解,Spring 扫描到这个类后会自动将这个 Bean 添加到容器里
@Component
public class UserDaoImp implements UserDao {
@Override
public void getUser() {
System.out.println("获取了数据");
}
}
等价于
<bean id="userDaoImp" class="com.alsritter.dao.UserDaoImp"/>
因为有三层架构,所以也有单独的三个注解(但是效果都和 @Component
一样,都是注册到 Spring 容器里),这些不同的注解只是用来标识不同层
@Repository
:dao层
@Service
:service层
@Controller
:controller层
如果要给字段赋值,直接在属性上(set 方法也行)加上 @Value
注解
@Value("小美")
String name;
等价于
<bean id="people" class="com.alsritter.pojo.People">
<property name="name" value="小美"/>
</bean>
配置作用域,效果同上
@Scope("singleton")
Spring 配置文件
Bean 别名
可以给 Bean 配置别名,且可以设置多个别名
<bean id="user" class="com.alsritter.pojo.User">
<property name="name" value="张三"/>
</bean>
<alias name="user" alias="user3"/>
Bean 标识
<!--
id:bean的唯一标识符,也就是相当于对象名
class:bean对象所对应的全限定名
name:也是别名,而且name可以取多个别名,可以用下例子的那些符号分隔
-->
<bean id="user2" class="com.alsritter.pojo.User" name="user4 user5;user6,user7">
<constructor-arg name="name" value="张三"/>
</bean>
导入其他的配置文件
Spring 可以创建多个配置文件再通过 import 整合起来
<import resource="beans.xml"/>
<import resource="beans2.xml"/>
<import resource="beans3.xml"/>
假设项目中有多个人协同开发,这三个人复制不同的类开发,不同的类需要注册在不同的 Bean 中,可以利用 import 将所有人的配置文件合并为一个总的,使用时直接用总的就可以了(一般叫 applicationContext.xml
)
注解的方式配置
就是使用注解来代替上面的 xml
配置文件
// 先在类上添加上这个Configuration注解,表名这个类是config
// 下面这个ComponentScan就是扫描包
// Import就是添加其他的配置类
@Configuration
@ComponentScan("com.alsritter.pojo")
@Import({MyConfig2.class,MyConfig3.class})
public class MyConfig {
// 注册一个Bean,就相当于我们之前写的一个Bean标签
// 这个方法的名字,就相当于bean标签中的id属性
// 这个方法的返回值就相当于bean标签中的class属性
// 注意,@Bean也是默认单例模式
@Bean
public User getUser(){
// 返回要注入到bean容器中的对象
return new User();
}
}
Bean 的配置
@Component
public class User {
@Value("小美")
private String name;
...
读取配置文件
public class MyTest {
public static void main(String[] args) {
/*
*如果完全使用了配置类方式去做,
* 我们就只能通过AnnotationConfigApplicationContext上下文来获取容器,
* 通过配置类的class对象加载
*/
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
//方法名就是Bean的名字
User getUser = (User) context.getBean("getUser");
System.out.println(getUser.getName());
}
}
自动装配 AutoWrite
Spring 会在上下文中自动寻找,并自动给 Bean 装配属性
有两种自动装配的方式,一种是通过 XML 文件的 byName
属性,另一种是使用注解 @Autowired
XML 的自动装配
XML的自动装配用法(只做了解,一般使用的是注解自动装配)
<!-- 以前手动引用对象的方式 -->
<bean id="cat" class="com.alsritter.pojo.Cat"/>
<bean id="dog" class="com.alsritter.pojo.Dog"/>
<bean id="people" class="com.alsritter.pojo.People">
<property name="name" value="小美"/>
<property name="cat" ref="cat"/>
<property name="dog" ref="dog"/>
</bean>
<!-- 使用自动装配 或使用byType-->
<bean id="people" class="com.alsritter.pojo.People" autowire="byName">
<property name="name" value="小美"/>
</bean>
id 和 name 的区别
自动装配的 byName 没有配置 name 时也可以使用 id,而 Spring 会给每个对象分配一个默认的 id
<bean id="/hello" class="com.alsritter.controller.HelloController"/>
配置文件中不允许出现两个 id 相同的,否则在初始化时即会报错
但配置文件中允许出现两个 name 相同的,在用 getBean()
返回实例时,后面一个 Bean 被返回,应该是前面那个被后面同名的 覆盖了。
为了避免不经意的同名覆盖的现象,尽量用 id 属性而不要用 name 属性。
注解的自动装配
要使用注解需要先添加支持
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 别忘了加上这句,开启注解的支持 -->
<context:annotation-config/>
<context:component-scan base-package="com.alsritter.pojo"/>
</beans>
直接在对象上面加上 @Autowired
注解
也可以加在 set 方法上
使用 @Autowired
之后可以不用再编写Set方法,前提是这个自动装配属性在 IoC 容器中存在,且符合名字 byName
因为框架采用的是反射,所以可以自动忽略访问修饰符
<bean id="cat" class="com.alsritter.pojo.Cat"/>
<bean id="dog" class="com.alsritter.pojo.Dog"/>
<bean id="people" class="com.alsritter.pojo.People"/>
使用注解自动装配
public class People {
@Autowired
private Cat cat;
...
注解指定注入对象
当名字不满足当前字段(或者不唯一),可以使用这个注解搭配 @Autowired
指定一个 Bean 给对象
注意:这个 @Autowired
默认注入方式是 byType
public class People {
@Autowired
@Qualifier("cat2222")
private Cat cat;
...
@Autowired 和 @Resource 的区别
补: 除了这个 @Autowired
自动注入注解,还有一个 效果一样 的注解 @Resource
@Resource
是java官方提供的注解,目的就是为了给这些容器框架一个标准,所以 Spring 也支持对这个注解进行注入,但是市面上容器框架不止一家,所以自己又做了个一样的注解 @Autowired
用来区分这个 @Resource
不过有一点,那就是 idea 不对 @Resource
进行检测(左边的标识)
@Autowired 和 @Resource 的区别
都是用来自动装配的,都可以放在属性字段上
@Autowired
通过 byType 的方式实现,而且必须要求这个对象存在!
@Resource
默认通过 byName 的方式实现,如果找不到名字,则通过 byType
注入 private 的原理
参考资料 spring通过注解方式依赖注入原理 (私有成员属性如何注入)
使用 Spring 的自动注入时会发现有时字段是使用 private 修饰的也能注入进来,而且没加 get set,也就没法通过反射拿到 get set 方法并注入它的依赖对象了,那它是怎么注入的呢?
如下代码:
@Service
public class Teacher {
@Resource
private Student student;
public void print(){
if(student!=null){
System.out.println("student name:"+student.getName());
}else{
System.out.println("student is null");
}
}
}
其实使用的还是反射,首先通过返回获取成员属性的注解,然后判断注解类型是根据对象类型还是名称注入,到这里都很好理解,关键在于私有对象如何注入,请看以下代码:
Field[] fields = Teacher.class.getDeclaredFields();
Student student = new Student();
student.setName("Alice");
Teacher teacher = new Teacher();
for (Field field : fields) {
if(field.getType().getName().equals(Student.class.getName())){
//关键点!设置私有成员属性为可访问!
field.setAccessible(true);
//将已创建的对象赋值
field.set(teacher, student);
}
}
teacher.print();
关键代码:
field.setAccessible(true);
使之可以直接读取私有字段